package me.angrybyte.numberpicker;

import me.angrybyte.numberpicker.utils.ColorStateList;
import me.angrybyte.numberpicker.utils.LogUtil;
import me.angrybyte.numberpicker.utils.ResUtil;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.ComponentState;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.PixelMapElement;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.components.element.StateElement;
import ohos.agp.render.BlendMode;
import ohos.agp.render.Canvas;
import ohos.agp.render.ColorFilter;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.render.Texture;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.app.Context;
import ohos.media.image.ImageSource;
import ohos.media.image.PixelMap;
import org.jetbrains.annotations.Nullable;

/**
 * Factory is used for creating colored elements for the whole app. <br>
 * Instantiates using the {@code Singleton} pattern, with the {@code get()} method.
 *
 * @version 1.3
 * @see ColorStateList
 * @see StateElement
 * @see Color
 */
public class Coloring {
    private static final String LOG_TAG = Coloring.class.getSimpleName();
    private static final int BOUNDS = 1500;
    private static final int BRIGHTNESS_THRESHOLD = 180;
    private static final int FADE_DURATION = 200;

    private static final Object mInitializerLock;
    private static Coloring mInstance;

    static {
        mInitializerLock = new Object();
    }

    /**
     * Destroys everything related to coloring.<br>
     */
    public static synchronized void destroy() {
        mInstance = null;
    }

    /**
     * Returns the singleton factory object.
     *
     * @return The only available {@code Coloring}
     */
    public static Coloring get() {
        if (mInstance == null) {
            synchronized (mInitializerLock) {
                if (mInstance == null) {
                    mInstance = new Coloring();
                }
            }
        }
        return mInstance;
    }

    /* **********  Factory methods go below this line  ********** */

    /**
     * Converts a String hex color value to an Integer color value.<br>
     * <br>
     * <b>Supported formats:</b><br>
     * <ul>
     * <li>#aaRRggBb</li>
     * <li>0xaaRRggBb</li>
     * <li>0XaaRRggBb</li>
     * <li>#RRggBb</li>
     * <li>0xRRggBb</li>
     * <li>0XRRggBb</li>
     * </ul>
     *
     * @param colorString String value of the desired color
     * @return Integer value for the color, or gray if something goes wrong
     */
    public Color decodeColor(String colorString) {
        if (colorString == null || colorString.trim().isEmpty())
            return Color.BLACK;

        if (colorString.startsWith("#"))
            colorString = colorString.replace("#", "");

        if (colorString.startsWith("0x"))
            colorString = colorString.replace("0x", "");

        if (colorString.startsWith("0X"))
            colorString = colorString.replace("0X", "");

        int alpha = -1, red = -1, green = -1, blue = -1;

        try {
            if (colorString.length() == 8) {
                alpha = Integer.parseInt(colorString.substring(0, 2), 16);
                red = Integer.parseInt(colorString.substring(2, 4), 16);
                green = Integer.parseInt(colorString.substring(4, 6), 16);
                blue = Integer.parseInt(colorString.substring(6, 8), 16);
            } else if (colorString.length() == 6) {
                alpha = 255;
                red = Integer.parseInt(colorString.substring(0, 2), 16);
                green = Integer.parseInt(colorString.substring(2, 4), 16);
                blue = Integer.parseInt(colorString.substring(4, 6), 16);
            }
            return new Color(Color.argb(alpha, red, green, blue));
        } catch (NumberFormatException e) {
            LogUtil.warn(LOG_TAG, "Error parsing color " + e);
            return Color.GRAY;
        }
    }

    /**
     * Blends given color with white background. This means that a full color<br>
     * with transparency (alpha) will be lightened to make it look like it is<br>
     * rendered over a white background. Resulting color will be non-transparent.
     *
     * @param color Color to use for blending
     * @return Lightened color to match a white underlay render
     */
    public int alphaBlendWithWhite(RgbColor color) {
        float alpha = color.getAlpha() / 255f;
        int origR = color.getRed();
        int origG = color.getGreen();
        int origB = color.getBlue();
        int white = 255;

        // rule: outputRed = (foregroundRed * foregroundAlpha) + (backgroundRed * (1.0 - foregroundAlpha))
        int r = (int) ((origR * alpha) + (white * (1.0 - alpha)));
        if (r > 255)
            r = 255;
        int g = (int) ((origG * alpha) + (white * (1.0 - alpha)));
        if (g > 255)
            g = 255;
        int b = (int) ((origB * alpha) + (white * (1.0 - alpha)));
        if (b > 255)
            b = 255;

        return Color.argb(255, r, g, b);
    }

    /**
     * Makes the given color a little bit darker.
     *
     * @param color Original color that needs to be darker
     * @return Darkened original color
     */
    public int darkenColor(RgbColor color) {
        int amount = 30;

        int r = color.getRed();
        int g = color.getGreen();
        int b = color.getBlue();
        int a = color.getAlpha();

        if (r - amount >= 0) {
            r -= amount;
        } else {
            r = 0;
        }

        if (g - amount >= 0) {
            g -= amount;
        } else {
            g = 0;
        }

        if (b - amount >= 0) {
            b -= amount;
        } else {
            b = 0;
        }

        return Color.argb(a, r, g, b);
    }

    /**
     * Makes the given color a little bit lighter.
     *
     * @param color Original color that needs to be lighter
     * @return Lightened original color
     */
    public int lightenColor(RgbColor color) {
        int amount = 60;

        int r = color.getRed();
        int g = color.getGreen();
        int b = color.getBlue();
        int a = color.getAlpha();

        if (r + amount <= 255) {
            r += amount;
        } else {
            r = 255;
        }

        if (g + amount <= 255) {
            g += amount;
        } else {
            g = 255;
        }

        if (b + amount <= 255) {
            b += amount;
        } else {
            b = 255;
        }

        return Color.argb(a, r, g, b);
    }

    /**
     * Creates a new drawable (implementation of the Drawable object may vary depending on OS version).<br>
     * Drawable will be colored with given color, and clipped to match given boundaries.
     *
     * @param color Integer color used to color the output drawable
     * @param bounds Four-dimensional vector bounds
     * @return Colored and clipped drawable object
     */
    @SuppressWarnings("UnusedDeclaration")
    public Element createDrawable(Color color, Rect bounds) {
        // init normal state drawable
        ShapeElement drawable = new ShapeElement();
        drawable.setRgbColor(RgbColor.fromArgbInt(color.getValue()));
        if (color == Color.TRANSPARENT) {
            drawable.setAlpha(0);
        }
        drawable.setBounds(bounds);
        return drawable;
    }

    /**
     * Colors the given drawable to a specified color. Uses mode SRC_ATOP.
     *
     * @param context Which context to use
     * @param drawable Which drawable to color
     * @param color Which color to use
     * @return A colored drawable ready for use
     */
    public Element colorDrawable(Context context, @Nullable Element drawable, Color color) {
        if (!(drawable instanceof PixelMapElement)) {
            LogUtil.warn(LOG_TAG, "Original drawable is not a bitmap! Trying with constant state cloning.");
            return colorUnknownDrawable(drawable, color);
        }

        PixelMap original = ((PixelMapElement) drawable).getPixelMap();
        PixelMap copy = ResUtil.createPixelmap(original.getImageInfo().size.width, original.getImageInfo().size.height, original.getImageInfo().pixelFormat);

        Paint paint = new Paint();
        Canvas c = new Canvas(new Texture(copy));
        //paint.setColorFilter(new ColorFilter(color.getValue(), BlendMode.SRC_ATOP));
        c.drawPixelMapHolder(new PixelMapHolder(original), 0, 0, paint);

        return new PixelMapElement(copy);
    }

    /**
     * Colors the given drawable to a specified color set using the drawable wrapping technique.
     *
     * @param drawable Which drawable to color
     * @param colorStates Which color set to use
     * @return A colored drawable ready to use
     */
    public Element colorDrawableWrap(Element drawable, ColorStateList colorStates) {
        if (drawable != null) {
            drawable.setStateColorList(colorStates.getStates(), colorStates.getColors());
            drawable.setStateColorMode(BlendMode.SRC_ATOP);
            return drawable;
        }
        return null;
    }

    /**
     * Colors the given drawable to a specified color using the drawable wrapping technique.
     *
     * @param drawable Which drawable to color
     * @param color Which color to use
     * @return A colored drawable ready to use
     */
    public Element colorDrawableWrap(Element drawable, int color) {
        if (drawable != null) {
            int[][] state = new int[][]{};
            int[] c = new int[]{color};
            drawable.setStateColorList(state, c);
            drawable.setStateColorMode(BlendMode.SRC_ATOP);
            return drawable;
        }
        return null;
    }

    /**
     * Tries to clone and just color filter the drawable. Uses mode SRC_ATOP.
     *
     * @param drawable Which drawable to color
     * @param color Which color to use
     * @return A colored drawable ready for use
     */
    public Element colorUnknownDrawable(@Nullable Element drawable, Color color) {
        if (drawable != null) {
            int[][] state = new int[][]{};
            int[] c = new int[]{color.getValue()};
            drawable.setStateColorList(state, c);
            drawable.setStateColorMode(BlendMode.SRC_ATOP);
        }
        return drawable;
    }

    /**
     * Colors the given drawable to a specified color. Uses mode SRC_ATOP.<br>
     * Automatically loads a good quality bitmap from the {@code resourceId} if it is valid.
     *
     * @param context Which context to use
     * @param resourceId Which drawable resource to load
     * @param color Which color to use
     * @return A colored {@link Element} ready for use
     */
    public Element colorDrawable(Context context, int resourceId, int color) {
        ImageSource.DecodingOptions opts = new ImageSource.DecodingOptions();

        PixelMap original = ResUtil.getDrawable(context, resourceId, opts);
        return colorDrawable(context, new PixelMapElement(original), new Color(color));
    }

    /**
     * Creates a new {@code StateElement} drawable. States that should be provided are "normal",<br>
     * "clicked" (pressed) and "checked" (selected). All states are actually integer colors.<br>
     * Optionally, {@code shouldFade} can be set to false to avoid the fading effect.<br>
     * <br>
     * Note: <i>{@link Color#TRANSPARENT} can be used to supply a transparent state.</i>
     *
     * @param normal Color for the idle state
     * @param clicked Color for the clicked/pressed state
     * @param checked Color for the checked/selected state
     * @param shouldFade Set to true to enable the fading effect, false otherwise
     * @return A {@link StateElement} drawable object ready for use
     */
    public Element createStateDrawable(Color normal, Color clicked, Color checked, boolean shouldFade) {
        // init state arrays
        int[] selectedState = new int[] {
                ComponentState.COMPONENT_STATE_SELECTED
        };
        int[] pressedState = new int[] {
                ComponentState.COMPONENT_STATE_PRESSED
        };
        int[] checkedState = new int[] {
                ComponentState.COMPONENT_STATE_CHECKED
        };
        int[] focusedState = new int[] {
                ComponentState.COMPONENT_STATE_FOCUSED
        };
        int[] activatedState = new int[] {};

        // init normal state drawable
        ShapeElement normalDrawable = ResUtil.buildDrawableByColor(normal.getValue());
        if (normal == Color.TRANSPARENT)
            normalDrawable.setAlpha(0);

        // init clicked state drawable
        ShapeElement clickedDrawable = ResUtil.buildDrawableByColor(clicked.getValue());
        if (clicked == Color.TRANSPARENT)
            clickedDrawable.setAlpha(0);

        // init checked state drawable
        ShapeElement checkedDrawable = ResUtil.buildDrawableByColor(checked.getValue());
        if (checked == Color.TRANSPARENT)
            checkedDrawable.setAlpha(0);

        // init focused state drawable (use normal color)
        ShapeElement focusedDrawable = ResUtil.buildDrawableByColor(normal.getValue());
        if (normal == Color.TRANSPARENT)
            focusedDrawable.setAlpha(0);

        // prepare state list (order of adding states is important!)
        StateElement states = new StateElement();
        states.addState(pressedState, clickedDrawable);
        if (!shouldFade) {
            states.addState(selectedState, clickedDrawable);
            states.addState(focusedState, focusedDrawable);
            states.addState(checkedState, checkedDrawable);
        }

        // add fade effect if applicable
        if (shouldFade) {
            states.addState(new int[] {}, normalDrawable);
            states.setEnterFadeDuration(0);
            states.setExitFadeDuration(FADE_DURATION);
        } else {
            states.addState(activatedState, clickedDrawable);
            states.addState(new int[] {}, normalDrawable);
        }

        return states;
    }

    /**
     * Creates a new drawable using given parameters. States that should be provided are "normal",<br>
     * "clicked" (pressed) and "checked" (selected). All states are actually integer colors.<br>
     * Optionally, {@code shouldFade} can be set to false to avoid the fading effect.<br>
     * Drawable instance will be a StateElement.<br>
     * <br>
     * Note: <i>{@link Color#TRANSPARENT} can be used to supply a transparent state.</i>
     *
     * @param normal Color for the idle state
     * @param clicked Color for the clicked/pressed state
     * @param checked Color for the checked/selected state
     * @param shouldFade Set to true to enable the fading effect, false otherwise
     * @return A {@link StateElement} drawable object ready for use
     */
    public Element createBackgroundDrawable(Color normal, Color clicked, Color checked, boolean shouldFade) {
        return createBackgroundDrawable(normal, clicked, checked, shouldFade, null);
    }

    /**
     * Very similar to {@link #createBackgroundDrawable(Color, Color, Color, boolean)}, adding only one more parameter.
     *
     * @param normal Color state normal
     * @param clicked Color state clicked
     * @param checked Color state checked
     * @param shouldFade true if fading effect enabled
     * @param bounds Clip/mask drawable to these rectangle bounds
     * @return Clipped/masked drawable instance
     */
    public Element createBackgroundDrawable(Color normal, Color clicked, Color checked, boolean shouldFade, Rect bounds) {
        return createStateDrawable(normal, clicked, checked, shouldFade);
    }

    /**
     * Similar to {@link #createContrastStateDrawable(Context, Color, Color, boolean, Element)} but using colors
     * only, no drawables.
     *
     * @param normal Color normal state to this color
     * @param clickedBackground Background color of the View that will show when view is clicked
     * @return The color state list that is in contrast with the on-click background color
     */
    public ColorStateList createContrastStateColors(Color normal, Color clickedBackground) {
        // init state arrays
        int[] normalState = new int[] {};
        int[] selectedState = new int[] {
                ComponentState.COMPONENT_STATE_SELECTED
        };
        int[] pressedState = new int[] {
                ComponentState.COMPONENT_STATE_PRESSED
        };
        int[] checkedState = new int[] {
                ComponentState.COMPONENT_STATE_CHECKED
        };
        int[] activatedState = new int[] {};

        // initialize identifiers
        int[] stateColors;
        int[][] stateIdentifiers;
        int contrastColor = getContrastColor(clickedBackground).getValue();

        stateIdentifiers = new int[][] {
                selectedState, pressedState, checkedState, activatedState, normalState
        };
        stateColors = new int[] {
                contrastColor, contrastColor, contrastColor, contrastColor, normal.getValue()
        };

        return new ColorStateList(stateIdentifiers, stateColors);
    }

    /**
     * Similar to {@link #createBackgroundDrawable(Color, Color, Color, boolean)} but with additional {@code original} drawable parameter.
     *
     * @param context Which context to use
     * @param normal Color normal state of the drawable to this color
     * @param clickedBackground Background color of the View that will show when view is clicked
     * @param shouldFade Set to true if the state list should have a fading effect
     * @param original This drawable will be contrasted to the {@code clickedBackground} color on press
     * @return The state list drawable that is in contrast with the on-click background color
     */
    public Element createContrastStateDrawable(Context context, Color normal, Color clickedBackground, boolean shouldFade, Element original) {
        if (original == null || original instanceof StateElement) {
            if (original != null) {
                LogUtil.info(LOG_TAG, "Original drawable is already a StateElement");
                original = original.getCurrentElement();
            }

            // overridden in previous if clause, so check again
            if (original == null) {
                return null;
            }
        }

        // init state arrays
        int[] selectedState = new int[] {
                ComponentState.COMPONENT_STATE_SELECTED
        };
        int[] pressedState = new int[] {
                ComponentState.COMPONENT_STATE_PRESSED
        };
        int[] checkedState = new int[] {
                ComponentState.COMPONENT_STATE_CHECKED
        };
        int[] activatedState = new int[] {};

        Element normalStateDrawable = colorDrawable(context, original, normal);
        Element clickedStateDrawable = colorDrawable(context, original, getContrastColor(clickedBackground));
        Element checkedStateDrawable = colorDrawable(context, original, getContrastColor(clickedBackground));

        // prepare state list (order of adding states is important!)
        StateElement states = new StateElement();
        states.addState(pressedState, clickedStateDrawable);
        if (!shouldFade) {
            states.addState(selectedState, clickedStateDrawable);
            states.addState(checkedState, checkedStateDrawable);
        }

        // add fade effect if applicable
        if (shouldFade) {
            states.addState(new int[] {}, normalStateDrawable);
            states.setEnterFadeDuration(0);
            states.setExitFadeDuration(FADE_DURATION);
        } else {
            states.addState(activatedState, clickedStateDrawable);
            states.addState(new int[] {}, normalStateDrawable);
        }

        return states;
    }

    /**
     * This basically chooses between {@link #createContrastStateDrawable(Context, Color, Color, boolean, Element)}
     * depending on the available API level.
     *
     * @param context Which context to use
     * @param normal Color normal state of the drawable to this color
     * @param clickedBackground Background color of the View that will show when view is clicked
     * @param shouldFade Set to true if the state list (pre-API 21) should have a fading effect
     * @param original This drawable will be contrasted to the {@code clickedBackground} color on press
     *
     * @return The state list drawable that is in contrast with the on-click background color
     */
    public Element createContrastBackgroundDrawable(Context context, Color normal, Color clickedBackground, boolean shouldFade,
                                                    Element original) {
        return createContrastStateDrawable(context, normal, clickedBackground, shouldFade, original);
    }

    /**
     * Calculates the contrasted color from the given one. If the color darkness is under<br>
     * the {@link #BRIGHTNESS_THRESHOLD}, contrasted color is white. If the color darkness is<br>
     * over the {@link #BRIGHTNESS_THRESHOLD}, contrasted color is black.
     *
     * @param color Calculating contrasted color to this one
     * @return White or black, depending on the provided color's brightness
     */
    public Color getContrastColor(Color color) {
        RgbColor rgbColor = RgbColor.fromArgbInt(color.getValue());
        int r = rgbColor.getRed();
        int g = rgbColor.getGreen();
        int b = rgbColor.getBlue();

        // human eye is least sensitive to blue, then to red, then green; calculating:
        int brightness = (b + r + r + g + g + g) / 6;

        if (brightness < BRIGHTNESS_THRESHOLD)
            return Color.WHITE;
        else
            return Color.BLACK;
    }
}
